﻿// <copyright file="Scheme.cs" company="Joe Chung">
// Copyright © 2008 by Joe Chung.
// </copyright>
namespace NScheme
{
    using System;
    using System.IO;

    public sealed class Scheme : NScheme.SchemeUtils
    {
        internal InputPort input = new InputPort(Console.In);

        internal PrintWriter output = new PrintWriter(Console.Out, true);

        internal Environment globalEnvironment = new Environment();

        public Scheme(string[] files)
        {
            Primitive.installPrimitives(this.globalEnvironment);
            try
            {
                this.load(new InputPort(new StringReader(SchemePrimitives.CODE)));
                for (int i = 0; i < (files == null ? 0 : files.Length); i++)
                {
                    this.load(files[i]);
                }
            }
            catch (Exception)
            {
            }
        }

        public static void Main(string[] args)
        {
            new Scheme(args).readEvalWriteLoop();
        }

        public object eval(object x)
        {
            return this.eval(x, this.globalEnvironment);
        }

        public object eval(object x, Environment env)
        {
            while (true)
            {
                if (x is string)
                {
                    // VARIABLE
                    return env.lookup((string)x);
                }
                else if (!(x is Pair))
                {
                    // CONSTANT
                    return x;
                }
                else
                {
                    object fn = first(x);
                    object args = rest(x);
                    if (fn as string == "quote")
                    {
                        // QUOTE
                        return first(args);
                    }
                    else if (fn as string == "begin")
                    {
                        // BEGIN
                        for (; rest(args) != null; args = rest(args))
                        {
                            this.eval(first(args), env);
                        }

                        x = first(args);
                    }
                    else if (fn as string == "define")
                    {
                        // DEFINE
                        if (first(args) is Pair)
                        {
                            return env.define(first(first(args)), this.eval(cons("lambda", cons(rest(first(args)), rest(args))), env));
                        }
                        else
                        {
                            return env.define(first(args), this.eval(second(args), env));
                        }
                    }
                    else if (fn as string == "set!")
                    {
                        // SET!
                        return env.set(first(args), this.eval(second(args), env));
                    }
                    else if (fn as string == "if")
                    {
                        // IF
                        x = truth(this.eval(first(args), env)) ? second(args) : third(args);
                    }
                    else if (fn as string == "cond")
                    {
                        // COND
                        x = this.reduceCond(args, env);
                    }
                    else if (fn as string == "lambda")
                    {
                        // LAMBDA
                        return new Closure(first(args), rest(args), env);
                    }
                    else if (fn as string == "macro")
                    {
                        // MACRO
                        return new Macro(first(args), rest(args), env);
                    }
                    else
                    {
                        // PROCEDURE CALL:
                        fn = this.eval(fn, env);
                        if (fn is Macro)
                        {
                            // MACRO
                            x = ((Macro)fn).expand(this, (Pair)x, args);
                        }
                        else if (fn is Closure)
                        {
                            // CLOSURE CALL
                            Closure f = (Closure)fn;
                            x = f.body;
                            env = new Environment(f.parms, this.evalList(args, env), f.env);
                        }
                        else
                        {
                            // OTHER PROCEDURE CALL
                            return Procedure.proc(fn).apply(this, this.evalList(args, env));
                        }
                    }
                }
            }
        }

        public Pair evalList(object list, Environment env)
        {
            if (list == null)
            {
                return null;
            }
            else if (!(list is Pair))
            {
                error("Illegal arg list: " + list);
                return null;
            }
            else
            {
                return cons(this.eval(first(list), env), this.evalList(rest(list), env));
            }
        }

        public object load(object fileName)
        {
            string name = stringify(fileName, false);
            try
            {
                return this.load(new InputPort(new FileStream(name, FileMode.Open, FileAccess.Read)));
            }
            catch (IOException)
            {
                return error("can't load " + name);
            }
        }

        public object load(InputPort inp)
        {
            object x;
            for (; ; )
            {
                if (InputPort.isEOF(x = inp.read()))
                {
                    return TRUE;
                }

                this.eval(x);
            }
        }

        public void readEvalWriteLoop()
        {
            object x;
            for (; ; )
            {
                try
                {
                    this.output.print("> ");
                    this.output.flush();
                    if (InputPort.isEOF(x = this.input.read()))
                    {
                        return;
                    }

                    write(this.eval(x), this.output, true);
                    this.output.println();
                    this.output.flush();
                }
                catch (Exception)
                {
                }
            }
        }

        public object reduceCond(object clauses, Environment env)
        {
            object result = null;
            for (; ; )
            {
                if (clauses == null)
                {
                    return FALSE;
                }

                object clause = first(clauses);
                clauses = rest(clauses);
                if (first(clause) as string == "else" || truth(result = this.eval(first(clause), env)))
                {
                    if (rest(clause) == null)
                    {
                        return list("quote", result);
                    }
                    else if (second(clause) as string == "=>")
                    {
                        return list(third(clause), list("quote", result));
                    }
                    else
                    {
                        return cons("begin", rest(clause));
                    }
                }
            }
        }
    }
}